import pandas as pd
import numpy as np
def data_split(data_path,x=0.8,random=False):
    '''
    切分数据集，保证用户数量保持不变，将每个用户的评分数据按比例进行切分
    :param data_path: 数据集的路径
    :param x: 训练集的比例
    :param random: 是否是随机切分
    :return: 用户-物品评分矩阵
    '''
    print("开始切分数据集、、、、、")
    # 设置要加载的数据字段的类型
    dtype={"userId":np.int32,"movieId":np.int32,"rating":np.float32}
    # 加载数据，上面的三列数据
    ratings=pd.read_csv(data_path,dtype=dtype,usecols=range(3))
    testset_index=[]
    # 保证每个用户在训练集和测试集都有数据，按userId进行聚合
    for uid in ratings.groupby("userId").any().index:
        user_rating_data=ratings.where(ratings["userId"]==uid).dropna()
        if random:
            # 因为不可变类型不能被shuffle方法作用，所以需要强行转换为列表
            index=list(user_rating_data.index)
            np.random.shuffle(index)  #打乱列表
            _index=round(len(user_rating_data)*x)
            testset_index+=list(index[_index:])
        else:
#             将每个用户的x比例的数据作为训练集，剩余的作为测试集
            index=round(len(user_rating_data)*x)
            testset_index+=list(user_rating_data.index.values[index:])
    testset=ratings.loc[testset_index]
    trainset=ratings.drop(testset_index)
    print("完成数据集的切分....")
    return trainset,testset
def accuray(predict_results,method="all"):
    '''
    准确性指标计算方法
    :param predict_results:
    :param method:
    :return:
    '''
    def rmse(predict_results):
        '''
        rmse评估指标
        :param predict_results:
        :return:
        '''
        length=0
        _rmse_sum=0
        for uid,iid,real_rating,pred_rating in predict_results:
            length=1
            _rmse_sum+=(pred_rating-real_rating)**2
        return round(np.sqrt(_rmse_sum/length),4)
    def mae(predict_results):
        '''
        mae评估指标
        :param predict_results:
        :return:
        '''
        length=0
        _mae_sum=0
        for uid,iid,real_rating,pred_rating in predict_results:
            length+=1
            _mae_sum+=abs(pred_rating-real_rating)
        return round(_mae_sum/length,4)
    def rmse_mae(predict_results):
        '''
        rmse和mae评估指标
        :param predict_results:
        :return:
        '''
        length=0
        _rmse_sum=0
        _mae_sum=0
        for uid,iid,real_rating,pred_rating in predict_results:
            length+=1
            _rmse_sum+=(pred_rating-real_rating)**2
            _mae_sum+=abs(pred_rating-real_rating)
        return round(np.sqrt(_rmse_sum/length),4),round(_mae_sum/length,4)
    if method.lower()=="rmse":
        rmse(predict_results)
    elif method.lower()=="mae":
        mae(predict_results)
    else:
        return rmse_mae(predict_results)

class BaselineCFBySGD(object):
    def __init__(self,number_epochs,alpha,reg,columns=["uid","iid","rating"]):
#         #梯度下降最高的迭代次数
        self.number_epochs=number_epochs
#       学习率
        self.alpha=alpha
#       正则参数
        self.reg=reg
#       数据集中  user   item  rating 字段
        self.columns=columns
    def fit(self,dataset):
        '''
        :param dataset: uid,iid,rating
        :return:
        '''
        self.dataset=dataset
#         用户评分数据
        self.users_ratings=dataset.groupby(self.columns[0]).agg([list])[[self.columns[1],self.columns[2]]]
#         物品评分数据
        self.items_ratings=dataset.groupby(self.columns[1]).agg([list])[[self.columns[0],self.columns[2]]]
#         全局评分
        self.global_mean=self.dataset[self.columns[2]].mean()
#         调用sgd方法训练模型参数
        self.bu,self.bi=self.sgd()
    def sgd(self):
        '''
        利用随机梯度下降，优化bu,bi的值
        :return: bu,bi
        '''
        bu=dict(zip(self.users_ratings.index,np.zeros(len(self.users_ratings))))
        bi=dict(zip(self.items_ratings.index,np.zeros(len(self.items_ratings))))
        for i in range(self.number_epochs):
            print("正在迭代：iter{:}".format(i))
            for uid,iid,real_rating in self.dataset.itertuples(index=False):
                error=real_rating-(self.global_mean+bu[uid]+bi[iid])
                bu[uid]+=self.alpha*(error-self.reg*bu[uid])
                bi[iid]+=self.alpha*(error-self.reg*bi[iid])
        return bu,bi
    def predict(self,uid,iid):
        '''
        评分预测
        :param uid:
        :param iid:
        :return:
        '''
        if iid not in self.items_ratings.index:
            raise  Exception("无法预测用户  {uid} 对点电影{iid}的评分，因训练集缺失{iid}的数据".format(uid=uid,iid=iid))
        predict_rating=self.global_mean+self.bu[uid]+self.bi[iid]
        return predict_rating
    def test(self,testset):
        '''
        预测测试集数据
        :param testset:
        :return:
        '''
        for uid ,iid ,real_rating in testset.itertuples(index=False):
            try:
                pred_rating=self.predict(uid,iid)
                print("uid:{},iid:{},real_rating:{},pred_rating:{}".format(uid,iid,real_rating,pred_rating))
            except Exception as e:
                print(e)
            else:
                yield  uid,iid,real_rating,pred_rating
if __name__ == '__main__':
    trainset,testset=data_split("ratings.csv",random=True)
    bcf=BaselineCFBySGD(2,0.1,0.2,["userId","movieId","rating"])
    bcf.fit(trainset)

    pred_results=bcf.test(testset)
    rmse,mae=accuray(pred_results)

    # dtype=[("userId",np.int32),("movieId",np.int32),("rating",np.float32)]
    # dataset=pd.read_csv("ratings.csv",usecols=range(3),dtype=dict(dtype))
    # bcf=BaselineCFBySGD(15,0.1,0.5,["userId","movieId","rating"])
    # bcf.fit(dataset)
    # while True:
    #     uid=int(input("uid:"))
    #     iid=int(input("iid:"))
    #     print(bcf.predict(uid,iid))
    # print("pred_rating:"+len(pred_results))
    # for u,i,r in pred_results:
    #     print("用户：{}，电影：{}，预测的评分{}".format(u,i,r))
    print("rmse:{:},mae:{:}".format(rmse,mae))
#     自适应调参
